- Funktionen
- Pricing

Der Next.js App Router hat eine Reihe nützlicher Features eingeführt, wie beispielsweise die Unterstützung von React Server Components, besseres Caching, Streaming-Antworten und verschachtelte Layouts. Diese Features ermöglichen es Entwicklern, eine verbesserte Benutzererfahrung zu schaffen und gleichzeitig von einer besseren Entwicklererfahrung zu profitieren. Obwohl diese Features hilfreich und benutzerfreundlich sind, können sie bei falscher Verwendung zu Fehlern oder Performance-Problemen führen.
In diesem Artikel gehen wir auf die häufigsten Fehler ein, die du beim Next.js App Router machen kannst. Außerdem stellen wir Lösungen mit Code-Beispielen vor, die helfen, diese Fehler zu vermeiden und die Benutzererfahrung zu verbessern.
Beachte, dass die Beispiele in diesem Artikel die Next.js-Projektstruktur „/src/app“ verwenden.
Das Abrufen von Daten auf der Client-Seite ist in React seit vielen Jahren gängige Praxis. Daher tappt man leicht in die Falle, redundante Netzwerkanfragen in der Client-Komponente zu stellen, was aufgrund der zusätzlichen Netzwerklatenz nicht besonders effizient ist und dich dazu zwingt, verschiedene HTTP-Route-Handler zu erstellen.
Im folgenden Beispiel ruft die „<UserComponent>“ Daten auf der Client-Seite mithilfe des „useEffect“-Hooks ab. Dieser Ansatz funktioniert zwar, ist aber nicht ideal, da er zu mehr Netzwerkanfragen auf der Client-Seite führt und die Benutzererfahrung verlangsamen kann.
"use client";
import { useEffect, useState } from "react";
const UserComponent = () => {
const [data, setData] = useState<{ name: string; email: string } | null>(
null
);
useEffect(() => {
const fetchData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const result = await res.json();
setData(result);
};
fetchData();
}, []);
if (!data) return <div>Loading...</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponent;Um das zu beheben, kannst du eine Serverkomponente erstellen, die Daten auf der Serverseite abruft und sie in der „UserComponentFixed“-Komponente auf dem Server rendert:
const fetchUser = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Failed to fetch user");
return res.json();
};
const UserComponentFixed = async () => {
const data = await fetchUser(); // Fetching on the server side
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserComponentFixed;Dadurch wird sichergestellt, dass der Client einen HTML-Code erhält, was die Belastung des Clients beim Abrufen und Rendern der Daten verringert und somit die allgemeine Benutzererfahrung verbessert.
Der Next.js App Router verfolgt einen „Static-First“-Ansatz, was bedeutet, dass Seiten immer als statisch gerendert werden, sofern sie nicht explizit dynamische APIs verwenden. Dieser Ansatz verbessert zwar die performance der Anwendung, kann aber zu Problemen führen, wenn die Seite fälschlicherweise als statisch identifiziert wird, obwohl du sie dynamisch rendern möchtest, aber keine herkömmlichen dynamischen Trigger verwendest.
Um dies zu beheben, solltest du die connection-Funktion verwenden, um explizit die dynamische Darstellung zu aktivieren. Dies stellt sicher, dass die Seitenlogik, wie das Generieren einer zufälligen ID oder das Abrufen aktueller Daten, bei jeder Anfrage ausgeführt wird:
// src/app/page.tsx
import { connection } from "next/server";
export function random(min: number, max: number) {
return Math.floor(Math.random() * (max - min) + min);}
export const fetchUser = async (id: number) => {
const res = await fetch("https://jsonplaceholder.typicode.com/users/" + id);
if (!res.ok) throw new Error("Failed to fetch user");
const data = await res.json();
return { ...data };};
export default async function Home() {
// Explicitly opt into dynamic rendering
await connection();
const userId = random(1, 10);
const data = await fetchUser(userId);
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);}
Mit Server-Aktionen kannst du Funktionen auf dem Server von den Server- und Client-Komponenten aus ausführen, beispielsweise für Anwendungsfälle wie das Absenden von Formularen und Datenänderungen.
Während du eine Serveraktion direkt innerhalb einer Serverkomponente erstellen kannst, ist dies bei Client-Komponenten nicht möglich. Um Serveraktionen sicher innerhalb von Client-Komponenten zu definieren und zu verwenden, musst du Serveraktionen in einer separaten Datei definieren und in die Client-Komponenten importieren. Dadurch stellst du sicher, dass die Serveraktion nur auf dem Server ausgeführt wird und das Programmieren aus dem clientseitigen Bundle ausgeschlossen ist.
Wenn du eine Client-Komponente für eine To-Do-Aufgabe erstellst, ist es sinnvoll, eine separate Datei für die Serveraktion zu erstellen und diese in die Client-Komponente zu importieren, wie im folgenden Beispiel gezeigt:
"use server";
// src/app/actions.ts
export async function completeTodo(id: string) {
// server action to perform todo completion
}
"use client";
// src/components/TodoItem.tsx
import { completeTodo } from "@/app/actions";
export function TodoItem() {
const id = "todo_23";
return <button onClick={() => completeTodo(id)}>Complete</button>;
}Mit React Suspense kannst du einen Ladestatus anzeigen, während auf eine Antwort gewartet wird. Du musst jedoch auch wissen, wo die Suspense-Grenzen platziert werden müssen, um sicherzustellen, dass dies die performance der Anwendung verbessert und eine gute Benutzererfahrung bietet.
Das folgende Beispiel setzt eine Suspense-Grenze für die gesamte Seite:
// app/page.tsx
import { Suspense } from "react";
import UserProfile from "./components/UserProfile";
import RecentPosts from "./components/RecentPosts";
export default function Page() {
return (
<Suspense fallback={<div>Loading page...</div>}>
<UserProfile />{" "}
{/* Both components are delayed if one is still loading */}
<RecentPosts />
</Suspense>
);
}Dies blockiert das Rendern, bis alle Komponenten auf der Seite vollständig geladen sind. Dies kann zu langen Ladezeiten führen und die performance der Anwendung beeinträchtigen.
Es ist sinnvoller, die Suspense-Grenze für jede Komponente zu setzen, die Daten lädt:
// app/page.tsx
import { Suspense } from "react";
import UserProfile from "./components/UserProfile";
import RecentPosts from "./components/RecentPosts";
export default function Page() {
return (
<div>
<h1>Welcome to the Dashboard</h1>
{/* Place Suspense boundaries around each section */}
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile /> {/* Shows loading state only for this component */}
</Suspense>
<Suspense fallback={<div>Loading recent posts...</div>}>
<RecentPosts /> {/* Shows a different loading state for posts */}
</Suspense>
</div>
);
}Auf diese Weise kann der Benutzer die Dashboard-Seite mit Ladekonturen sehen, und die Komponenten werden beim Laden gerendert, ohne sich gegenseitig zu blockieren.
Der App Router bietet Hilfsfunktionen, um direkt in den Routen-Handlern und den Serverkomponenten auf Anforderungsdaten wie Header und Cookies zuzugreifen. Du kannst diese Funktionen jedoch nicht in den Client-Komponenten verwenden, da es sich um serverseitige Hilfsfunktionen handelt. Stattdessen kannst du in den Client-Komponenten die Hooks „usePathname“, „useSearchParams“ und „useParams“ verwenden, um auf Informationen zur aktuellen Route zuzugreifen.
Wenn du in der Client-Komponente auf die Request-Header oder Cookies zugreifen musst, kannst du dies in einer Server-Komponente tun und den abgeleiteten Wert an die Client-Komponente übergeben. Im folgenden Beispiel muss die Client-Komponente „UserProfile“ auf den Namen des angemeldeten Benutzers zugreifen, der nur aus dem gesicherten Cookie abgerufen werden kann:
// src/components/UserProfile.tsx
"use client"; // Marked as a client component
type UserProfileProps = {
userName: string;
};
export default function UserProfile({ userName }: UserProfileProps) {
// some interactive logic for the client component
return (
<div>
<h2>User Information</h2>
<p>Logged in as: {userName}</p>
</div>
);
}Um dies zu lösen, kannst du eine Serverkomponente erstellen, die den Namen des Benutzers aus dem Cookie abruft und an die Clientkomponente übergibt:
// src/app/page.tsx
import { cookies } from "next/headers";
import { UserProfile } from "./components/UserProfile";
export default async function Page() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
const user = await getUserFromSession(session); // this is a custom function
return (
<div>
<UserProfile userName={user.name} />
</div>
);
}React Context ist eine reine Client-API und wird in Serverkomponenten nicht unterstützt. Daher würde die direkte Verwendung von Context im Root-Layout nicht funktionieren, wie im folgenden Beispiel gezeigt:
// src/app/layout.tsx
import { createContext } from "react";
// createContext is not supported in Server Components
export const ThemeContext = createContext({});
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
);
}Um dieses Problem zu beheben, kannst du eine Client-Komponente erstellen, die den Context exportiert und ihn in der Server-Komponente verwendet:
// src/components/ThemeProvider.tsx
"use client";
import { createContext } from "react";
export const ThemeContext = createContext({});
export default function ThemeProvider({
children,
}: {
children: React.ReactNode;
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// src/app/layout.tsx
import ThemeProvider from "./components/ThemeProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}Es ist besser, die Context-Provider so tief wie möglich in der Anwendungsstruktur zu verwenden, damit Next.js so viel wie möglich optimieren kann. Wenn der „ThemeProvider“ also nur in der Route „/dashboard/settings“ verwendet wird, rende ihn in der nächstgelegenen „layout.tsx“-Datei, um die beste performance zu erzielen.
Server- und Client-Komponenten arbeiten zusammen, haben aber jeweils eine spezifische Funktion. Verwende Client-Komponenten für jegliche Zustandsverwaltung und Interaktivität. Du kannst React-Hooks innerhalb einer Client-Komponente verwenden, jedoch nicht innerhalb einer Server-Komponente, wie im folgenden Beispiel:
// src/components/CheckoutForm.tsx
"use client";
import { useState, useEffect } from "react";
export default function CheckoutForm() {
const [name, setName] = useState("");
useEffect(() => {
// some effect logic
}, []);
return <form>{/* interactive form */}</form>;
}Serverkomponenten eignen sich aufgrund der zusätzlichen Sicherheits- und Performance-Vorteile am besten für das Abrufen von Daten. Sie sind auch vorzuziehen, wenn du umfangreiche Bibliotheken für die Datenbearbeitung benötigst, wie z. B. Bibliotheken für Datenverwaltung oder Datenauswertung. Hier ist ein Beispiel:
// src/components/UserDetails.tsx
import { fetchUser } from "./UserAvatar";
const UserDetailsComponent = async () => {
const data = await fetchUser(5);
return (
<div>
No store:
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};
export default UserDetailsComponent;Das folgende Beispiel veranschaulicht die Verwendung von Client- und Serverkomponenten auf derselben Seite:
// src/app/page.tsx
import UserDetails from "./components/UserDetails";
export default function Page() {
// The Page function is a server component
return (
<div>
<h1>Dashboard</h1>
{/* Including a client component to handle dynamic state */}
<CheckoutForm />
<UserDetails />
</div>
);
}Die neueste Next.js-Version unterstützt auch die veraltete Verzeichnisstruktur des Pages Routers, um einen reibungslosen Übergang für bestehende Projekte zu ermöglichen, die die Features des App Routers schrittweise übernehmen möchten.
Die Funktion `getServerSideProps` wird verwendet, um die Daten serverseitig abzurufen und das Ergebnis an die Seitenkomponente zu übergeben. Bei der Migration kann es daher verlockend sein, die Funktion in einen Routen-Handler umzuwandeln und sie über einen `fetch`-API-Aufruf aufzurufen. Durch das Hinzufügen von React Server Components kannst du den serverseitigen Code zum Abrufen von Daten jedoch direkt in eine Serverkomponente einbinden.
Im folgenden Beispiel wird die Funktion `getServerSideProps` verwendet, um Daten auf der Serverseite abzurufen und an die Seitenkomponente `Orders` zu übergeben, und die Komponente `Orders` wird dann verwendet, um die Daten auf der Clientseite zu rendern:
// pages/orders.js (Pages Router)
import React from "react";
export default function Orders({ orders }) {
return (
<div>
<h1>Your Orders</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Order #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}
// Fetch data for the page with getServerSideProps
export async function getServerSideProps() {
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return {
props: {
orders,
},
};
}Im App-Router kannst du die Bestelldaten direkt in der Serverkomponente „OrdersPage“ abrufen und serverseitig rendern:
// src/app/orders/page.tsx (App Router, Server Component)
export default async function OrdersPage() {
// Fetch the data directly in the server component
const res = await fetch("https://api.example.com/orders");
const orders = await res.json();
return (
<div>
<h1>Your Orders</h1>
<ul>
{orders.map((order) => (
<li key={order.id}>
<p>
Order #{order.id}: {order.item}
</p>
</li>
))}
</ul>
</div>
);
}Ähnlich wird die Funktion „getStaticPaths“ im Pages Router verwendet, um eine Liste von Pfaden für eine statisch gerenderte dynamische Route zu generieren. Im App Router verwendest du die Funktion „generateStaticParams“, um ein Array von Params-Objekten für dynamische Routen wie folgt zu generieren:
import PostLayout from "./components/post-layout";
export async function generateStaticParams() {
return [{ id: "1" }, { id: "2" }];
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();
return post;
}
export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return <PostLayout post={post} />;
}Die Verwendung von Client-Komponenten innerhalb von Server-Komponenten bietet zwar erhebliche Möglichkeiten zur Komposition, kann aber manchmal auch die Grenzen zwischen Client- und Server-Code verwischen. Diese Unschärfe birgt das Risiko, dass sensibler serverseitiger Code oder Geheimnisse versehentlich dem Client zugänglich gemacht werden, indem sie fälschlicherweise in die Client-Komponente aufgenommen werden.
Um sicherzustellen, dass dies niemals passiert, verwende immer die Anweisung `use server` am Anfang der Deklarationen der Server-Aktionsfunktionen. Besser noch: Erstelle eine separate Datei `actions.ts`, die alle Server-Aktionen enthält, und verwende die Anweisung `use server` am Anfang oder auf Modulebene.
Du kannst auch das npm-Paket „server-only“ verwenden, um ein reines Servermodul explizit zu deklarieren. Wenn du beispielsweise ein Modul zum Abrufen von Daten namens „src/data.ts“ hast, kannst du das Modul „server-only“ installieren und es wie folgt verwenden, um den Code aus dem clientseitigen Bundle auszuschließen, selbst wenn er versehentlich importiert wird:
import "server-only";
export async function getUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}Eine wesentliche Änderung im modernen App Router besteht darin, dass params und searchParams nun asynchron sind. Du musst diese Objekte als Promises behandeln und auf sie warten, bevor du auf ihre Eigenschaften zugreifst. Ein synchroner Zugriff führt nun zu Fehlern während des Build-Prozesses.
Du kannst in deinen Seitenkomponenten wie folgt über die params-Prop auf den Slug zugreifen:
// src/app/blog/[slug]/page.tsx
// Note the async function and the Promise type for params
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
// You must await params before accessing the slug
const { slug } = await params;
return <div>My Post: {slug}</div>;
}Ebenso musst du in deinen API-Route-Handlern auf die params warten:
// src/app/api/users/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
// Await the params promise to get the slug
const { slug } = await params;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${slug}`);
const data = await res.json();
return Response.json({ data });
}In diesem Artikel haben wir einige der häufigsten Fallstricke bei der Verwendung des Next.js App Routers behandelt und Möglichkeiten besprochen, diese zu beheben und die allgemeine Benutzererfahrung zu verbessern.
Wenn du mit Next.js-Anwendungen entwickelst, kannst du diese auf Upsun bereitstellen, das Features wie vertikale und horizontale Skalierung, Edge-Caching und Observability von Haus aus bietet.